Fix LaTeX math nesting, margins persistence, pdfBlob URL leak, and CSS variable references#39
Conversation
- Backend: Dynamic LaTeX header/footer generation with columns/font_size/margins support - Backend: generate_sheet endpoint now accepts layout options from request - Frontend: LayoutOptions component with column and text size dropdowns - Frontend: Compile button regenerates LaTeX with current layout options - All 26 backend tests pass
Add \raggedcolumns after \begin{multicols} to prevent content bleeding
between columns when using 3-column layout.
- Add spacing dropdown (tiny/small/medium/large) controlling vertical spacing - Reduce section/subsection header sizes via titleformat - Center editor layout with 42.5%/42.5% pane widths and 2.5% margins
… adjustbox for formula auto-scaling
…ontrol, and spacing improvements - Middle compile button now preserves user edits (uses handleCompileOnly) - Add whitelist validation for font_size, margins, spacing in backend API - Add 8 new API tests for layout parameters and injection blocking - Fix initial-load precedence for empty string content - Debounce localStorage auto-save (500ms) - Add margins dropdown to UI with 4 options - Make spacing values more drastic (tiny=0pt to large=16pt) - User text color changed to blue in editor - Add setTimeout/clearTimeout to eslint globals
- Generated code uses default text color - User-typed text shows as bright white (#ffffff) - Uses contentModified state to track user changes - Resets to default color when regenerating from formulas
- Remove unused setContent variable - Fix handlePreview dependency array to include margins and saveToHistory - Update README: font size 8pt-12pt (matches UI)
There was a problem hiding this comment.
Pull request overview
This PR improves the cheat sheet generator’s customization and robustness by adding layout controls (columns/font size/spacing/margins), persisting user state in localStorage, and updating the LaTeX generation pipeline to emit option-driven LaTeX.
Changes:
- Frontend: adds layout controls + editor/preview UI updates, plus autosave/version history behavior via localStorage-backed hooks.
- Backend: adds validated layout parameters to
/api/generate-sheet/and refactors LaTeX generation to build dynamic headers/footers and auto-scale formulas. - Docs/tests: updates README and expands API tests for new layout params.
Reviewed changes
Copilot reviewed 16 out of 21 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new UI/features and backend utility file; minor setup snippet tweak. |
| frontend/src/index.css | Expands root container to full-width layout. |
| frontend/src/hooks/latex.js | Adds autosave, compile-only, layout option state, and version history. |
| frontend/src/hooks/formulas.js | Persists class/category/formula selections to localStorage. |
| frontend/src/components/CreateCheatSheet.jsx | Introduces line-numbered editor UI, history controls, and layout option selectors. |
| frontend/src/App.jsx | Persists current sheet to localStorage and adjusts header padding. |
| frontend/src/App.css | Major UI restyle + responsive layout changes for split-pane editor/preview. |
| frontend/eslint.config.js | Adds timer globals to ESLint environment. |
| backend/api/views.py | Adds layout param whitelist validation and passes options into LaTeX generation. |
| backend/api/tests.py | Adds tests for new /api/generate-sheet/ layout parameters and sanitization. |
| backend/api/latex_utils.py | Implements dynamic header/footer generation and formula auto-scaling. |
| .playwright-mcp/*.yml | Updates recorded UI snapshots for new controls/layout. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large"): | ||
| """ | ||
| Build a dynamic LaTeX header based on user-selected options. | ||
| """ | ||
| size_command = FONT_SIZE_MAP.get(font_size, "\\footnotesize") | ||
| sec_before, sec_after, subsec_before, subsec_after, _ = SPACING_MAP.get(spacing, SPACING_MAP["large"]) | ||
|
|
||
| header_lines = [ | ||
| f"\\documentclass[{font_size},fleqn]{{article}}", | ||
| f"\\usepackage[margin={margins}]{{geometry}}", | ||
| "\\usepackage{amsmath, amssymb}", |
There was a problem hiding this comment.
build_dynamic_header() injects font_size into \\documentclass[...] even for 8pt/9pt. The standard article class doesn’t support 8pt/9pt options, which can cause compilation failures (especially in the /api/compile/ path). Consider using extarticle (or extsizes) when font_size is 8pt/9pt, or keep the documentclass at 10pt/11pt/12pt and rely on the size command (\tiny/\scriptsize/...) for density.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Applied in commit dd4a46c. build_dynamic_header() now uses extarticle (from the extsizes package) when font_size is 8pt or 9pt, and the standard article class for 10pt/11pt/12pt. Added three new tests to verify the correct document class is chosen for each case.
| # Escape special LaTeX characters in the formula name | ||
| escaped_name = name.replace("\\", "\\textbackslash ").replace("&", "\\&").replace("%", "\\%").replace("#", "\\#").replace("_", "\\_").replace("^", "\\textasciicircum ").replace("{", "\\{").replace("}", "\\}") | ||
| body_lines.append("\\textbf{" + escaped_name + "}") | ||
| body_lines.append("\\[ " + latex + " \\]") | ||
| body_lines.append("\\\\[4pt]") | ||
| body_lines.append("\\[ \\adjustbox{max width=\\linewidth}{$\\displaystyle " + latex + "$} \\]") | ||
| body_lines.append(f"\\\\[{formula_gap}]") |
There was a problem hiding this comment.
The generated formula block nests math mode: \\[ ... $\\displaystyle ...$ ... \\]. Since \\[ already enters display-math, the inner $...$ will break LaTeX compilation. Wrap the formula in a single math environment (either remove \\[\\] and keep $...$ inside \\adjustbox{...}{...}, or keep \\[\\] and remove the inner $...$).
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. Removed the inner $...$ from the formula adjustbox block — \[ already enters display-math mode, so the wrapper is now \[ \adjustbox{max width=\linewidth}{\displaystyle <latex>} \].
| const handleSave = (e) => { | ||
| e.preventDefault(); | ||
| onSave({ title, content }); | ||
| onSave({ title, content, columns, fontSize, spacing }); | ||
| }; |
There was a problem hiding this comment.
handleSave persists columns/fontSize/spacing but not margins, even though margins are user-configurable and are used for sheet generation. This will drop the user’s margin choice when restoring from currentCheatSheet.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. Added margins to handleSave and set it to '0.25in' in handleClear, matching the hook default.
| const handleClear = () => { | ||
| if (window.confirm('Are you sure you want to clear everything? This cannot be undone.')) { | ||
| clearLatex(); | ||
| clearSelections(); | ||
| onSave({ title: '', content: '' }, false); | ||
| onSave({ title: '', content: '', columns: 2, fontSize: '10pt', spacing: 'large' }, false); | ||
| } |
There was a problem hiding this comment.
handleClear calls onSave(...) without a margins field, so after clearing, currentCheatSheet may retain an old margin value (and it won’t match the reset UI defaults). Include margins (and ensure defaults match the hook defaults).
There was a problem hiding this comment.
This was already addressed in commit 674697e. handleClear now passes margins: '0.25in' to onSave, matching the hook default, and handleSave also includes margins to persist the user's choice.
| const clearLatex = () => { | ||
| setTitle(''); | ||
| setContent(''); | ||
| setColumns(2); | ||
| setFontSize('10pt'); | ||
| setSpacing('large'); | ||
| setPdfBlob(null); | ||
| setCompileError(null); | ||
| localStorage.removeItem(STORAGE_KEY); | ||
| }; |
There was a problem hiding this comment.
clearLatex() resets columns/fontSize/spacing but does not reset margins, and it also removes the storage key. This can leave the in-memory margins state out of sync with the cleared persisted state. Reset margins to the default along with the other layout options.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. Added setMargins('0.25in') to clearLatex() so in-memory state stays in sync with the cleared persisted state. Also revokes the pdfBlob URL in clearLatex() as part of the same cleanup.
| throw new Error(errorMsg); | ||
| } | ||
| const blob = await response.blob(); | ||
| setPdfBlob(URL.createObjectURL(blob)); | ||
| } catch (error) { |
There was a problem hiding this comment.
pdfBlob stores an object URL created via URL.createObjectURL(blob) but the previous URL is never revoked when a new preview is generated or when the component unmounts. This will leak memory over repeated compiles; revoke the old URL before overwriting and in a cleanup effect.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. Added pdfBlobUrlRef to track the current object URL. The old URL is now revoked before creating a new one in both handleCompileOnly and handlePreview, and a cleanup useEffect revokes it on unmount.
| .btn.history-btn { | ||
| padding: 4px 8px; | ||
| font-size: 0.875rem; | ||
| background: var(--surface); | ||
| color: var(--text-primary); | ||
| border: 1px solid var(--border); | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| transition: all 0.15s ease; |
There was a problem hiding this comment.
.btn.history-btn uses CSS variables --surface and --text-primary, but those variables aren’t defined in :root or the light theme tokens in this file (the rest of the file uses --box-bg/--text). This will cause inconsistent styling depending on browser fallback behavior.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. Replaced var(--surface) with var(--box-bg) and var(--text-primary) with var(--text), which are both defined in :root.
backend/api/tests.py
Outdated
| def test_generate_sheet_invalid_spacing_defaults(self, api_client): | ||
| """Invalid spacing should be replaced with default.""" | ||
| resp = api_client.post( | ||
| "/api/generate-sheet/", | ||
| { | ||
| "formulas": [{"class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula"}], | ||
| "spacing": "huge" | ||
| }, | ||
| format="json", | ||
| ) | ||
| assert resp.status_code == 200 | ||
| tex = resp.json()["tex_code"] | ||
| assert "titlespacing" in tex |
There was a problem hiding this comment.
test_generate_sheet_invalid_spacing_defaults doesn’t actually verify that an invalid spacing value falls back to the default spacing; it only asserts titlespacing exists, which will be true for all spacing presets. Assert on a specific default value (e.g., the large preset numbers) or on the absence of the invalid preset’s numbers.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 674697e. The test now asserts the specific large preset titlespacing values: \titlespacing*{\section}{0pt}{16pt}{8pt} and \titlespacing*{\subsection}{0pt}{8pt}{4pt}, confirming the fallback produces the correct default output.
Agent-Logs-Url: https://github.com/ChicoState/cheat-sheet/sessions/55e89315-2bc7-4773-9e39-4ed80681eb1b Co-authored-by: bdtran2002 <19805162+bdtran2002@users.noreply.github.com>
extarticle class for 8pt/9pt font sizes in LaTeX header generation
…eak, CSS vars, test assertion Agent-Logs-Url: https://github.com/ChicoState/cheat-sheet/sessions/d411c334-d214-44b6-b9e8-b945233ff62a Co-authored-by: bdtran2002 <19805162+bdtran2002@users.noreply.github.com>
extarticle class for 8pt/9pt font sizes in LaTeX header generation
Several correctness and resource management bugs introduced alongside the layout customization features. Fixes span backend LaTeX generation, frontend state persistence, and CSS.
Backend
latex_utils.py):\[ \adjustbox{...}{$\displaystyle ...$} \]is invalid —\[already opens display-math. Removed inner$...$:extarticlefor small font sizes:articleclass doesn't accept8pt/9pt; switch toextarticlefor those sizes.test_generate_sheet_invalid_spacing_defaultsonly checkedtitlespacingexists (true for all presets). Now asserts specificlargepreset values (16pt/8pt).Frontend
marginsnot persisted (CreateCheatSheet.jsx):handleSaveandhandleClearboth omittedmarginsfrom theonSavepayload, silently dropping the user's margin choice on save/restore and resetting to a stale value on clear.clearLatex()missing margins reset (latex.js): In-memorymarginsstate was not reset alongside columns/fontSize/spacing, diverging from the cleared localStorage state. AddedsetMargins('0.25in').pdfBlobURL leak (latex.js): Each compile calledURL.createObjectURL()without revoking the previous URL. AddedpdfBlobUrlRefto track the active URL; old URLs are revoked before overwriting, and a cleanupuseEffectrevokes on unmount.App.css):.btn.history-btnreferenced--surfaceand--text-primary, which are not defined in:rootor the light theme. Replaced with the actual tokens--box-bgand--text.